Current File : /home/jeconsul/public_html/wp-content/plugins/suremails/inc/emails/handler/process-email-data.php
<?php
/**
 * ProcessEmailData.php
 *
 * Handles processing of email data components such as recipients, headers, attachments, message, and subject.
 *
 * @package SureEmails\Inc\Emails\Handler
 */

namespace SureMails\Inc\Emails\Handler;

use PHPMailer\PHPMailer\Exception;
use SureMails\Inc\ConnectionManager;
use SureMails\Inc\Traits\Instance;
use SureMails\Inc\Utils\LogError;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Class ProcessEmailData
 *
 * Provides methods to process different components of an email.
 */
class ProcessEmailData {

	use Instance;

	/**
	 * Array of primary recipients with 'name' and 'email'.
	 *
	 * @var array
	 */
	private $to = [];

	/**
	 * Associative array containing 'name' and 'email' for the sender.
	 *
	 * @var array
	 */
	private $from = [
		'name'  => '',
		'email' => '',
	];

	/**
	 * Array of CC recipients with 'name' and 'email'.
	 *
	 * @var array
	 */
	private $cc = [];

	/**
	 * Array of BCC recipients with 'name' and 'email'.
	 *
	 * @var array
	 */
	private $bcc = [];

	/**
	 * Array of Reply-To addresses with 'name' and 'email'.
	 *
	 * @var array
	 */
	private $reply_to = [];

	/**
	 * Content type of the email (e.g., 'text/plain' or 'text/html').
	 *
	 * @var string
	 */
	private $content_type = 'text/plain';

	/**
	 * Character set of the email (e.g., 'UTF-8').
	 *
	 * @var string
	 */
	private $charset = 'UTF-8';

	/**
	 * Boundary string used in multipart emails.
	 *
	 * @var string
	 */
	private $boundary = '';

	/**
	 * Mailer used to send the email.
	 *
	 * @var string
	 */
	private $x_mailer;

	/**
	 * Associative array for any additional headers.
	 *
	 * @var array
	 */
	private $extra_headers = [];

	/**
	 * The email body content.
	 *
	 * @var string
	 */
	private $message = '';

	/**
	 * Array of file paths to be attached.
	 *
	 * @var array
	 */
	private $attachments = [];

	/**
	 * The email subject.
	 *
	 * @var string
	 */
	private $subject = '';

	/**
	 * Indicates if the email is a resend.
	 *
	 * @var bool
	 */
	private $is_resend = false;

	/**
	 * ProcessEmailData constructor.
	 *
	 * Initializes default values for properties.
	 */
	public function __construct() {
		$this->x_mailer  = 'WordPress/' . get_bloginfo( 'version' );
		$this->is_resend = ConnectionManager::instance()->get_is_resend();
	}

	/**
	 * Process the recipients ($to).
	 *
	 * @param string|array $to Recipients as a comma-separated string or an array.
	 * @return array Array of recipients with 'name' and 'email'.
	 */
	public function process_to( $to ) {
		$recipients = [];

		// If $to is a string, split it by commas.
		if ( ! is_array( $to ) ) {
			$to = explode( ',', $to );
		}

		foreach ( (array) $to as $recipient ) {
			$recipient = trim( $recipient );
			if ( empty( $recipient ) ) {
				continue;
			}

			// Check if recipient is in the format "Name <email@example.com>".
			if ( preg_match( '/^(.*)<(.+)>$/', $recipient, $matches ) ) {
				$name  = trim( $matches[1], " \t\n\r\0\x0B\"" ); // Remove surrounding quotes and whitespace.
				$email = sanitize_email( trim( $matches[2] ) );
			} else {
				$name  = '';
				$email = sanitize_email( $recipient );
			}

			if ( is_email( $email ) ) {
				$recipients[] = [
					'name'  => $name,
					'email' => $email,
				];
			}
		}

		$this->set_to( $recipients );

		return $recipients;
	}

	/**
	 * Process the headers ($headers).
	 *
	 * @param string|array $headers Headers as a string or an array.
	 * @return array Associative array of processed headers.
	 */
	public function process_headers( $headers ) {
		$processed_headers = [
			'bcc'           => [],
			'cc'            => [],
			'reply_to'      => [],
			'content_type'  => 'text/plain',
			'charset'       => get_bloginfo( 'charset' ),
			'boundary'      => '',
			'x_mailer'      => $this->x_mailer,
			'extra_headers' => [],
			'from'          => [
				'name'  => '',
				'email' => '',
			],
		];

		// If headers are a string, split them by newlines.
		if ( ! is_array( $headers ) ) {
			$headers = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
		}

		foreach ( (array) $headers as $header ) {
			$header = trim( $header );
			if ( empty( $header ) ) {
				continue;
			}

			// Ensure the header contains a colon.
			if ( strpos( $header, ':' ) === false ) {
				// Handle headers like "boundary=..." without a colon.
				if ( stripos( $header, 'boundary=' ) !== false ) {
					$boundary_parts = preg_split( '/boundary=/i', $header );
					if ( isset( $boundary_parts[1] ) ) {
						$processed_headers['boundary'] = trim( str_replace( [ '"', "'" ], '', $boundary_parts[1] ) );
					}
				}
				continue;
			}

			[$name, $content] = explode( ':', $header, 2 );
			$name             = strtolower( trim( $name ) );
			$content          = trim( $content );

			switch ( $name ) {
				case 'bcc':
					$processed_headers['bcc'] = array_merge( $processed_headers['bcc'], $this->parse_emails( $content ) );
					break;
				case 'cc':
					$processed_headers['cc'] = array_merge( $processed_headers['cc'], $this->parse_emails( $content ) );
					break;
				case 'reply-to':
					$processed_headers['reply_to'] = array_merge( $processed_headers['reply_to'], $this->parse_emails( $content ) );
					break;
				case 'content-type':
					// Split content-type and charset if available.
					if ( strpos( $content, ';' ) !== false ) {
						[$type, $params]                   = explode( ';', $content, 2 );
						$processed_headers['content_type'] = trim( $type );

						// Parse parameters.
						foreach ( explode( ';', $params ) as $param ) {
							if ( stripos( $param, 'charset=' ) !== false ) {
								$charset                      = trim( str_replace( [ 'charset=', '"' ], '', $param ) );
								$processed_headers['charset'] = sanitize_text_field( $charset );
							} elseif ( stripos( $param, 'boundary=' ) !== false ) {
								$boundary                      = trim( str_replace( [ 'boundary=', '"' ], '', $param ) );
								$processed_headers['boundary'] = sanitize_text_field( $boundary );
							}
						}
					} else {
						$processed_headers['content_type'] = sanitize_text_field( $content );
					}
					break;
				case 'x-mailer':
					$processed_headers['x_mailer'] = sanitize_text_field( $content );
					break;
				case 'from':
					// Parse "From" header.
					if ( preg_match( '/^(.*)<(.+)>$/', $content, $matches ) ) {
						$name  = trim( $matches[1], " \t\n\r\0\x0B\"" ); // Remove surrounding quotes and whitespace.
						$email = sanitize_email( trim( $matches[2] ) );
						if ( is_email( $email ) ) {
							$processed_headers['from'] = [
								'name'  => $name,
								'email' => $email,
							];
						}
					} else {
						$email = sanitize_email( trim( $content ) );
						if ( is_email( $email ) ) {
							$processed_headers['from'] = [
								'name'  => '',
								'email' => $email,
							];
						}
					}
					break;
				default:
					// Any other headers.
					$processed_headers['extra_headers'][ $name ] = sanitize_text_field( $content );
					break;
			}
		}

		// Apply WordPress filters if 'from' is not set via headers.
		if ( empty( $processed_headers['from']['email'] ) ) {

			$from_email = apply_filters( 'wp_mail_from', '' );
			$from_name  = apply_filters( 'wp_mail_from_name', '' );

			if ( is_email( $from_email ) ) {
				$processed_headers['from'] = [
					'name'  => sanitize_text_field( $from_name ),
					'email' => $from_email,
				];
			}
		}

		// Apply WordPress filters for content type and charset.
		$processed_headers['content_type'] = apply_filters( 'wp_mail_content_type', $processed_headers['content_type'] );
		$processed_headers['charset']      = apply_filters( 'wp_mail_charset', $processed_headers['charset'] );

		// Set the processed headers to class properties.
		$this->set_from( $processed_headers['from'] );
		$this->set_cc( $processed_headers['cc'] );
		$this->set_bcc( $processed_headers['bcc'] );
		$this->set_reply_to( $processed_headers['reply_to'] );
		$this->set_content_type( $processed_headers['content_type'] );
		$this->set_charset( $processed_headers['charset'] );
		$this->set_boundary( $processed_headers['boundary'] );
		$this->set_x_mailer( $processed_headers['x_mailer'] );
		$this->set_extra_headers( $processed_headers['extra_headers'] );

		return $processed_headers;
	}

	/**
	 * Process the attachments ($attachments).
	 *
	 * @param string|array $attachments Attachments as a string or an array.
	 * @return array Array of sanitized attachment file paths.
	 */
	public function process_attachments( $attachments ) {
		$processed_attachments          = [];
		$processed_uploaded_attachments = [];

		// If attachments are a string, split them by newlines.
		if ( ! is_array( $attachments ) ) {
			$attachments = explode( "\n", str_replace( "\r\n", "\n", $attachments ) );
		}
		$upload = Uploads::instance();

		// Process each attachment.
		$uploaded_attachments = $upload->handle_attachments( $attachments );
		foreach ( (array) $attachments as $attachment ) {
			$attachment = trim( $attachment );
			if ( empty( $attachment ) ) {
				continue;
			}
			if ( $this->is_resend === true ) {
				$path = Uploads::get_suremails_base_dir();
				if ( ! is_wp_error( $path ) && isset( $path['path'] ) ) {
					$attachment = $path['path'] . '/attachments/' . $attachment;
				}
			}
			// Validate the attachment path.
			$attachment = sanitize_text_field( $attachment );
			if ( is_readable( $attachment ) ) {
				$processed_attachments[] = $attachment;
			}
		}

		foreach ( (array) $uploaded_attachments as $attachment ) {
			$attachment = trim( $attachment );
			if ( empty( $attachment ) ) {
				continue;
			}

			// Validate the attachment path.
			$attachment = sanitize_text_field( $attachment );
			if ( file_exists( $attachment ) && is_readable( $attachment ) ) {
				$processed_uploaded_attachments[] = $attachment;
			}
		}

		$this->set_attachments( $processed_attachments );

		return $processed_uploaded_attachments;
	}

	/**
	 * Process the message ($message).
	 *
	 * @param string $message The email message.
	 * @param bool   $is_html Whether the message is HTML.
	 * @return string The sanitized message.
	 */
	public function process_message( string $message, bool $is_html = false ) {

		$this->set_message( $message );

		return $message;
	}

	/**
	 * Sanitize and process the subject ($subject).
	 *
	 * @param string $subject The email subject.
	 * @return string The sanitized subject.
	 */
	public function process_subject( string $subject ) {

		$this->set_subject( $subject );

		return $subject;
	}

	/**
	 * Formats the processed headers into a suitable format for sending.
	 *
	 * @param array $headers Processed headers.
	 * @return array Formatted headers.
	 */
	public function format_processed_headers( array $headers ) {
		$formatted_headers = [];

		// From.
		if ( ! empty( $headers['from']['email'] ) ) {
			$from = $headers['from']['email'];
			if ( ! empty( $headers['from']['name'] ) ) {
				$from = $headers['from']['name'] . ' <' . $headers['from']['email'] . '>';
			}
			$formatted_headers[] = 'From: ' . $from;
		}

		// CC.
		if ( ! empty( $headers['cc'] ) ) {
			$cc_emails           = array_map(
				static function ( $cc ) {
					return ! empty( $cc['name'] ) ? "{$cc['name']} <{$cc['email']}>" : $cc['email'];
				},
				$headers['cc']
			);
			$formatted_headers[] = 'Cc: ' . implode( ', ', $cc_emails );
		}

		// BCC.
		if ( ! empty( $headers['bcc'] ) ) {
			$bcc_emails          = array_map(
				static function ( $bcc ) {
					return ! empty( $bcc['name'] ) ? "{$bcc['name']} <{$bcc['email']}>" : $bcc['email'];
				},
				$headers['bcc']
			);
			$formatted_headers[] = 'Bcc: ' . implode( ', ', $bcc_emails );
		}

		// Reply-To.
		if ( ! empty( $headers['reply_to'] ) ) {
			$reply_to_emails     = array_map(
				static function ( $reply_to ) {
					return ! empty( $reply_to['name'] ) ? "{$reply_to['name']} <{$reply_to['email']}>" : $reply_to['email'];
				},
				$headers['reply_to']
			);
			$formatted_headers[] = 'Reply-To: ' . implode( ', ', $reply_to_emails );
		}

		// Content-Type.
		if ( ! empty( $headers['content_type'] ) ) {
			$content_type_header = 'Content-Type: ' . $headers['content_type'];
			if ( ! empty( $headers['charset'] ) ) {
				$content_type_header .= '; charset=' . $headers['charset'];
			}
			if ( ! empty( $headers['boundary'] ) ) {
				$content_type_header .= '; boundary="' . $headers['boundary'] . '"';
			}
			$formatted_headers[] = $content_type_header;
		}

		// X-Mailer.
		if ( ! empty( $headers['x_mailer'] ) ) {
			$formatted_headers[] = 'X-Mailer: ' . $headers['x_mailer'];
		}

		// Extra Headers.
		if ( ! empty( $headers['extra_headers'] ) && is_array( $headers['extra_headers'] ) ) {
			foreach ( $headers['extra_headers'] as $name => $content ) {
				$formatted_headers[] = "{$name}: {$content}";
			}
		}

		// Return as array for consistency.
		return $formatted_headers;
	}

	/**
	 * Formats email recipients for logging.
	 *
	 * @param array|string $recipients The email recipients.
	 * @return string Formatted recipients.
	 */
	public function format_email_recipients( $recipients ) {
		$formatted = [];

		if ( ! is_array( $recipients ) ) {
			$recipients = array_map( 'trim', explode( ',', $recipients ) );
		}

		foreach ( $recipients as $recipient ) {

			if ( ! empty( $recipient['name'] ) ) {
				$formatted[] = "{$recipient['name']} <{$recipient['email']}>";
			} elseif ( ! empty( $recipient['email'] ) ) {
				$formatted[] = $recipient['email'];
			}
		}
		return implode( ', ', $formatted );
	}

	/**
	 * Comprehensive processing of all email components.
	 *
	 * @param string|array $to          Recipients as a comma-separated string or an array.
	 * @param string|array $headers     Headers as a string or an array.
	 * @param string       $message     The email message.
	 * @param string|array $attachments Attachments as a string or an array.
	 * @param string       $subject     The email subject.
	 * @return array Processed email data.
	 */
	public function process_all( $to, $headers, $message, $attachments, $subject ) {
		// Process each component.
		$processed_to          = $this->process_to( $to );
		$processed_headers     = $this->process_headers( $headers );
		$processed_message     = $this->process_message( $message, true );
		$processed_attachments = $this->process_attachments( $attachments );
		$processed_subject     = $this->process_subject( $subject );

		$mail_data = compact( 'to', 'headers', 'message', 'attachments', 'subject', );
		// Structure the processed data.
		$processed_data = [
			'to'                   => $this->get_to(),
			'headers'              => [
				'from'          => $this->get_from(),          // Array of name and email.
				'cc'            => $this->get_cc(),            // Array of name and email.
				'bcc'           => $this->get_bcc(),           // Array of name and email.
				'reply_to'      => $this->get_reply_to(),      // Array of name and email.
				'content_type'  => $this->get_content_type(),
				'charset'       => $this->get_charset(),
				'boundary'      => $this->get_boundary(),
				'x_mailer'      => $this->get_x_mailer(),
				'extra_headers' => $this->get_extra_headers(), // Associative array.
			],
			'message'              => $this->get_message(),
			'attachments'          => $this->get_attachments(),
			'subject'              => $this->get_subject(),
			'uploaded_attachments' => $processed_attachments,
		];

		$phpmailer = ConnectionManager::instance()->get_phpmailer();
		//phpcs:disable
		$phpmailer->clearAllRecipients();
		$phpmailer->clearAttachments();
		$phpmailer->clearCustomHeaders();
		$phpmailer->clearReplyTos();
		$phpmailer->Body    = '';
		$phpmailer->AltBody = '';

		// Populate PHPMailer with processed data.
		try {
			// Set From.
			$from       = $processed_data['headers']['from'];
			$from_email = $from['email'];
			$from_name  = $from['name'];

			if( ! empty( $from_email ) ) {
				$phpmailer->setFrom( $from_email, $from_name );
			}

			// Add To recipients.
			foreach ( $processed_data['to'] as $recipient ) {
				$phpmailer->addAddress( $recipient['email'], $recipient['name'] );
			}

			// Add CC recipients.
			if ( ! empty( $processed_data['headers']['cc'] ) ) {
				foreach ( $processed_data['headers']['cc'] as $cc ) {
					$phpmailer->addCC( $cc['email'], $cc['name'] );
				}
			}

			// Add BCC recipients.
			if ( ! empty( $processed_data['headers']['bcc'] ) ) {
				foreach ( $processed_data['headers']['bcc'] as $bcc ) {
					$phpmailer->addBCC( $bcc['email'], $bcc['name'] );
				}
			}

			// Add Reply-To addresses.
			if ( ! empty( $processed_data['headers']['reply_to'] ) ) {
				foreach ( $processed_data['headers']['reply_to'] as $reply_to ) {
					$phpmailer->addReplyTo( $reply_to['email'], $reply_to['name'] );
				}
			}

			// Set Subject.
			$phpmailer->Subject = $processed_data['subject'];

			// Set Body.
			if( strtolower( $processed_data['headers']['content_type'] ) === 'text/html' ) {
				$phpmailer->Body = $processed_data['message'];
				$phpmailer->AltBody = wp_strip_all_tags($processed_data['message']);
			} else {
				$phpmailer->Body    =  wp_strip_all_tags($processed_data['message']);
			}

			// Add Attachments.
			if ( ! empty( $processed_data['attachments'] ) ) {
				foreach ( $processed_data['attachments'] as $attachment ) {
					$file_name = $this->get_attachment_name( $attachment );
					$phpmailer->addAttachment($attachment, $file_name);
				}
			}

			// Set Content-Type and Charset.
			$phpmailer->isHTML( strtolower( $processed_data['headers']['content_type'] ) === 'text/html' );
			$phpmailer->CharSet = $processed_data['headers']['charset'];
			//phpcs:enable

		} catch ( Exception $e ) {
			// Handle exceptions during PHPMailer setup.
			LogError::instance()->log_error( 'PHPMailer Exception: ' . $e->getMessage() );
			do_action( 'wp_mail_failed', new \WP_Error( 'phpmailer_exception', $e->getMessage(), $mail_data ) );
		}

		return $processed_data;
	}

	/**
	 * Get the attachment name.
	 *
	 * @param string $attachment The attachment path.
	 * @return string The attachment name.
	 */
	public function get_attachment_name( $attachment ) {

		if ( $this->is_resend === false ) {
			return basename( $attachment );
		}

		$base_name = basename( $attachment );
		return substr( $base_name, strpos( $base_name, '-' ) + 1 );
	}

	/**
	 * Get HTML headers.
	 *
	 * @return string
	 */
	public static function get_html_headers() {
		return 'Content-Type: text/html; charset=UTF-8.';
	}

	/**
	 * Get plain text headers.
	 *
	 * @return string
	 */
	public static function get_text_headers() {
		return 'Content-Type: text/plain; charset=UTF-8.';
	}

	/* ==================== Getter and Setter Methods ==================== */

	/**
	 * Get the primary recipients.
	 *
	 * @return array Array of recipients with 'name' and 'email'.
	 */
	public function get_to() {
		return $this->to;
	}

	/**
	 * Set the primary recipients.
	 *
	 * @param array $to Array of recipients with 'name' and 'email'.
	 * @return void
	 */
	public function set_to( array $to ) {
		$this->to = $to;
	}

	/**
	 * Get the 'from' details.
	 *
	 * @return array Associative array with 'name' and 'email'.
	 */
	public function get_from() {
		return $this->from;
	}

	/**
	 * Set the 'from' details.
	 *
	 * @param array $from Associative array with 'name' and 'email'.
	 * @return void
	 */
	public function set_from( array $from ) {
		$this->from = $from;
	}

	/**
	 * Get the CC recipients.
	 *
	 * @return array Array of CC recipients with 'name' and 'email'.
	 */
	public function get_cc() {
		return $this->cc;
	}

	/**
	 * Set the CC recipients.
	 *
	 * @param array $cc Array of CC recipients with 'name' and 'email'.
	 * @return void
	 */
	public function set_cc( array $cc ) {
		$this->cc = $cc;
	}

	/**
	 * Get the BCC recipients.
	 *
	 * @return array Array of BCC recipients with 'name' and 'email'.
	 */
	public function get_bcc() {
		return $this->bcc;
	}

	/**
	 * Set the BCC recipients.
	 *
	 * @param array $bcc Array of BCC recipients with 'name' and 'email'.
	 * @return void
	 */
	public function set_bcc( array $bcc ) {
		$this->bcc = $bcc;
	}

	/**
	 * Get the Reply-To addresses.
	 *
	 * @return array Array of Reply-To addresses with 'name' and 'email'.
	 */
	public function get_reply_to() {
		return $this->reply_to;
	}

	/**
	 * Set the Reply-To addresses.
	 *
	 * @param array $reply_to Array of Reply-To addresses with 'name' and 'email'.
	 * @return void
	 */
	public function set_reply_to( array $reply_to ) {
		$this->reply_to = $reply_to;
	}

	/**
	 * Get the content type.
	 *
	 * @return string Content type of the email.
	 */
	public function get_content_type() {
		return $this->content_type;
	}

	/**
	 * Set the content type.
	 *
	 * @param string $content_type Content type of the email.
	 * @return void
	 */
	public function set_content_type( string $content_type ) {
		$this->content_type = $content_type;
	}

	/**
	 * Get the charset.
	 *
	 * @return string Character set of the email.
	 */
	public function get_charset() {
		return $this->charset;
	}

	/**
	 * Set the charset.
	 *
	 * @param string $charset Character set of the email.
	 * @return void
	 */
	public function set_charset( string $charset ) {
		$this->charset = $charset;
	}

	/**
	 * Get the boundary.
	 *
	 * @return string Boundary string used in multipart emails.
	 */
	public function get_boundary() {
		return $this->boundary;
	}

	/**
	 * Set the boundary.
	 *
	 * @param string $boundary Boundary string used in multipart emails.
	 * @return void
	 */
	public function set_boundary( string $boundary ) {
		$this->boundary = $boundary;
	}

	/**
	 * Get the X-Mailer.
	 *
	 * @return string Mailer used to send the email.
	 */
	public function get_x_mailer() {
		return $this->x_mailer;
	}

	/**
	 * Set the X-Mailer.
	 *
	 * @param string $x_mailer Mailer used to send the email.
	 * @return void
	 */
	public function set_x_mailer( string $x_mailer ) {
		$this->x_mailer = $x_mailer;
	}

	/**
	 * Get the extra headers.
	 *
	 * @return array Associative array of extra headers.
	 */
	public function get_extra_headers() {
		return $this->extra_headers;
	}

	/**
	 * Set the extra headers.
	 *
	 * @param array $extra_headers Associative array of extra headers.
	 * @return void
	 */
	public function set_extra_headers( array $extra_headers ) {
		$this->extra_headers = $extra_headers;
	}

	/**
	 * Get the message.
	 *
	 * @return string The email body content.
	 */
	public function get_message() {
		return $this->message;
	}

	/**
	 * Set the message.
	 *
	 * @param string $message The email body content.
	 * @return void
	 */
	public function set_message( string $message ) {
		$this->message = $message;
	}

	/**
	 * Get the attachments.
	 *
	 * @return array Array of attachment file paths.
	 */
	public function get_attachments() {
		return $this->attachments;
	}

	/**
	 * Set the attachments.
	 *
	 * @param array $attachments Array of attachment file paths.
	 * @return void
	 */
	public function set_attachments( array $attachments ) {
		$this->attachments = $attachments;
	}

	/**
	 * Get the subject.
	 *
	 * @return string The email subject.
	 */
	public function get_subject() {
		return $this->subject;
	}

	/**
	 * Set the subject.
	 *
	 * @param string $subject The email subject.
	 * @return void
	 */
	public function set_subject( string $subject ) {
		$this->subject = $subject;
	}

	/**
	 * Parse a string of email addresses into an array.
	 *
	 * @param string $emails Comma-separated email addresses.
	 * @return array Array of sanitized email addresses.
	 */
	private function parse_emails( string $emails ) {
		$parsed_emails = [];

		$emails = explode( ',', $emails );

		foreach ( $emails as $email ) {
			$email = trim( $email );
			if ( empty( $email ) ) {
				continue;
			}

			// Check if email is in "Name <email@example.com>" format.
			if ( preg_match( '/^(.*)<(.+)>$/', $email, $matches ) ) {
				$name  = trim( $matches[1], " \t\n\r\0\x0B\"" );
				$email = sanitize_email( trim( $matches[2] ) );
			} else {
				$name  = '';
				$email = sanitize_email( $email );
			}

			if ( is_email( $email ) ) {
				$parsed_emails[] = [
					'name'  => $name,
					'email' => $email,
				];
			}
		}

		return $parsed_emails;
	}
}